1import {Actor, Engine, EngineOptions, Scene} from "excalibur";2import {ReactElement, useEffect, useId, useRef, useState} from "react";34const engines = new Map<string, Engine>();56const cleanup = new Map<string, number>();78export type ExcaliburOptions = Omit<EngineOptions, 'canvasElementId' | 'canvasElement'>910type ExcaliburContainerProps = {11 className?: string,12 scene?: string,13 scenes?: Record<string, Scene>,14 actors?: Actor[],15 options?: ExcaliburOptions,16 isRunning?: boolean,17}1819const toggleEngine = (engine: Engine, shouldRun: boolean): void => {20 if (shouldRun && !engine.clock.isRunning()) {21 engine.clock.start();22 } else if (!shouldRun && engine.clock.isRunning()) {23 engine.once('postframe', () => engine.clock.stop());24 }25};2627const loadScenes = (engine: Engine, scenes: Record<string, Scene>): void => {28 for (const sceneName of Object.keys(scenes)) {29 if (engine.scenes[sceneName] === scenes[sceneName]) {30 continue;31 }3233 engine.addScene(sceneName, scenes[sceneName]);34 }35};3637const loadActors = (engine: Engine, actors: Actor[]): void => {38 const currentScene = engine.currentScene;39 for (const actor of actors) {40 if (currentScene.actors.includes(actor)) {41 continue;42 }4344 currentScene.add(actor);45 }46};4748export const ExcaliburContainer = ({49 options,50 scene,51 className,52 scenes = {},53 actors = [],54 isRunning = true,55 }: ExcaliburContainerProps): ReactElement => {56 const instanceId = useId();57 const canvasRef = useRef<HTMLCanvasElement>(null);58 const [currentSceneName, setCurrentSceneName] = useState<string | undefined>(scene);5960 useEffect(() => {61 const canvasElement = canvasRef.current;62 if (!canvasElement) {63 throw new Error('canvasRef.current should always return HTMLCanvasElement.');64 }6566 if (engines.has(instanceId)) {67 const cleanupTimeout = cleanup.get(instanceId);68 if (cleanupTimeout !== undefined) {69 clearTimeout(cleanupTimeout);70 }71 return;72 }7374 canvasElement.oncontextmenu = (): boolean => false;7576 const engine = new Engine({77 ...options,78 canvasElement,79 });8081 loadScenes(engine, scenes);8283 if (scene) {84 engine.goToScene(scene)85 .then(() => loadActors(engine, actors))86 .catch(() => console.error(`Unable to go to scene "${scene}`));87 }8889 engine.start().catch(() => console.log('Unable to start Excalibur engine'));9091 engines.set(instanceId, engine);9293 return (): void => {94 cleanup.set(instanceId, self.setTimeout(() => engine.stop(), 100));95 };96 }, [actors, instanceId, options, isRunning, scene, scenes]);9798 useEffect(() => {99 const engine = engines.get(instanceId);100 if (!engine) {101 return;102 }103104 loadScenes(engine, scenes);105106 loadActors(engine, actors);107108 if (scene && scene !== currentSceneName) {109 if (!Object.keys(engine.scenes).includes(scene)) {110 throw new Error(`Scene "${scene} is not loaded.`);111 }112113 engine.goToScene(scene)114 .then(() => setCurrentSceneName(engine.currentSceneName))115 .catch(() => console.error(`Unable to go to scene "${scene}`));116 } else {117 toggleEngine(engine, isRunning);118 }119 }, [instanceId, scenes, actors, scene, currentSceneName, isRunning]);120121 return <canvas122 id={instanceId}123 ref={canvasRef}124 className={className}125 ></canvas>;126};